1 - Introduction

首先老师谈为什么要在react中使用typescript,因为ts能提供很好的代码提示和维护体验。

image-20251103111302644

关于这个课程:

image-20251103111415221

2 - Getting Started

创建项目npm create vite@latest react-typescript-demo,将App.tsx里面清理一下,方便后面的学习。

3 - Typing Props

Typing Props,这里的typing是指加上类型,完整翻译就是给props(属性)加上类型。不是名词。

老师推荐使用type来定义ts类型,interface用在专门的定义文件里面比较多。

创建文件src\components\Greet.tsx,在App.tsx里面引用,引用的同时传入参数。这时候在Greet组件里面就需要定义ts类型。

效果:

image-20251104100416276

4 - Basic Props

基础属性。

这节课主要学习不同ts类型的props属性,number、boolean、object、array、

image-20251104102828062

5 - Advanced Props

高级属性。

联合类型

使用联合类型,可以规范用户的输入。

image-20251104103742280

children属性

children是字符串

image-20251104104504878

children是一个react组件

此时的children类型是React.ReactNode

image-20251104104630999

optional props

可选类型。在定义的属性后面添加?。此时可以解构出可选属性,赋默认值。

image-20251104105656193

6 - Event Props

事件属性。

这节课学习最常见的click事件和change事件,被当作props进行传递时,类型该怎么定义。

click事件

当click事件没有传递参数,也没有返回值时,定义很简单() => void

当需要使用到事件event参数时,此时的类型是React.MouseEvent,为了更加具体一点,可以为MouseEvent传入类型React.MouseEvent<HTMLButtonElement>

传递多个参数

使用到了高阶函数,也就是函数柯里化。这时候的event也要进行定义,即使之后用不上也要定义,但是可以在使用的时候在event前面加上_event,这样ts就会默认这个函数参数不会使用,就会不标记波浪线。

其余参数的类型定义就很简单了。

change事件

事件参数类型为React.ChangeEvent<HTMLInputElement>

可以看到触发了事件。input里面没有显示输入,是因为绑定了空字符串,这个不用管。

7 - Style Props

style代码被当作props传递时,应该怎么定义类型呢?

react专门提供了一个类型React.CSSProperties,用这个就可以。

image-20251104113721685

8 - Prop Types and Tips

这节课学习定义属性类型时的三个tips。

直接destructure props

在定义组件的时候,可以直接在小括号里面解构props,这样用起来可以省略props.的代码。推荐使用这种方式。

export types

在上面的学习中,我们在每个组件文件中都定义了类型,这对简单组件来说可以接受,但是对于复杂、大型的组件来说,将类型定义到一个单独的文件里面,并export导出这些类型,使用时导入类型,会更好。

定义一个Person.types.ts的文件,定义并导出类型。

reusing types

类型的重复使用。

将一些可以重复使用的类型抽离出来,这样就可以在不同的地方引入并使用了。

9 - useState Hook

type infer will take care of everything for simple values.

当在useState里面指定简单类型(比如说number、string、boolean等)的值时,ts会根据默认值推断出变量的类型,所以不需要特别指定类型。

image-20251104120434823

没有指定类型,也没有任何ts报错。

10 - useState Future Value

当使用useState定义变量时,只是在未来操作的时候知道值是什么,这时候该怎么定义类型呢?

这时候就不能依靠ts的类型推断了,要指定类型,传递给useState。

为什么要定义成null呢?不能定义成一个对象吗?里面的属性都为空字符串?

因为这个组件里面要判断是否登录,这个判断结果说不定要从API接口里面获取,如果已经登录了,那么就显示一部分信息(比如说欢迎界面),没有登录就显示另一部分信息(比如登录界面),要通过这个user来判断。

11 - useState Type Assertion

上面的案例中,我们会将默认值设置为null,但是有时候我们不想这么做,因为组件里面可能一直需要显示具体值。

这时候就可以使用类型断言。

12 - useReducer Hook

我们在使用useReducer的时候,ts会根据你传递的参数推断出类型。

const [state, dispatch] = useReducer(reducer, initialState);

那么重点就是设置reducer和initialState的类型。类型可以根据你的实际情况来写,很简单。

image-20251104141415932

可以看到,只有这里需要给出类型,那么我们就可以直接定义。

image-20251104141434253

可以看到useReducer里面使用时,不会要求给出类型,因为ts已经根据它的参数来推断出类型了。

并且,如果你想将state和dispatch以属性的方式传递的话,那么它们的类型也可以根据ts的提示来做,鼠标悬浮就可以看到:

image-20251104142242816

image-20251104142250052

13 - useReducer Strict Action Types

上节课中,将action的type类型设置为string,这意味着可以传任意的字符串进去,虽然reducer函数里面有default来返回当前值,但是作为开发者,为什么不能更加严格一点呢?

使用联合类型来解决。

image-20251104143204669

那如果reducer里面需要新增一个reset操作,不需要使用到payload,此时该怎么办呢?一般的方法就是将payload改为可选的。但此时increment和decrement的操作就需要添加判断。

image-20251104143445777

 

有更好的方法,就是将action的类型设置为联合类型,这样ts会根据类型的不同来推断操作,此时也不需要额外的判断条件。

image-20251104143833926

14 - useContext Hook

案例,为Box组件提供主题的上下文。

编写Box组件,非常简单:

创建提供上下文的组件:

下一步,使用上下文组件包裹Box组件:

下一步,在Box组件里面消费上下文。

可以看到,我们在使用useContext的时候,没有设置ts类型,但是没有任何报错,因为此时ts会执行类型推断。

image-20251104150602324

15 - useContext Future Value

这节课来探讨,如果我们事前不知道createContext的参数值,那么这么定义类型?这个案例很复杂,不止传递了值、还传递了方法。

案例:一个User上下文组件应该提供user的相关信息和setUser方法,所以我们会在上下文组件中定义一个user状态,但是这个上下文组件在用户没有登录的情况下,是没有用户信息的,所以此时的user是null。那么在用户登录之后,user的值就是一个对象。

问题就是此时在定义createContext的时候,类型应该怎么写?

用UserContextProvider包裹住要传递数据的组件:

在User组件里面使用上下文:

 

 

上面的代码中,在使用userContext的时候需要先判断,那么能不能不写判断呢?可以,还是按照上一个案例的方法,使用类型断言。

这样在User组件的handleLogin和handleLogout上就不需要先判断了。为什么可以这样呢?因为在src\components\context\UserContext.tsx组件中,使用了useState来定义状态,虽然user的值可能为null,但是setUser一定是一个函数,这是定义了状态之后就确定了。

这个案例非常经典,因为这个案例把clerk提供的登录功能是怎么实现的都讲清楚了,要搞懂这个案例。

16 - useRef Hook

访问DOM元素

案例:要求组件一加载完成就聚焦到input框。

image-20251104161048268

报错:'inputRef.current' is possibly 'null'.

那么就需要设置元素类型,并添加可选链操作符。

image-20251104161406714

如果你非常确定这个组件一定会渲染,你可以给useRef添加non-null assertion,同时可以去掉可选链操作符。

image-20251104161834038

保存值(不触发重新渲染)

这个案例是保存一个interval值。

image-20251104163141923

因为window.setInterval的返回值是数值类型,所以需要将useRef的类型添加上number类型。

image-20251104163348043

但还是有报错,这是因为interValRef.current可能为undefined,所以添加一个判断即可。

image-20251104163420078

最后的代码:

image-20251104163638076

 

17 - Class Component

这节课学习怎么在class定义的组件里面定义类型。

image-20251104164108132

可以看到有两个地方的ts报错,为此我们需要定义state和props的ts类型,并将ts类型和组件连接起来。

image-20251104164450299

18 - Component Prop

这节课学习how to pass a component with react and typescript.

之前是将组件当作children传到另一个组件中的,类型是React.ReactNode。但是这里不同,是这样传递的<FatherComponent component={SonComponent} />,这时候该怎么定义属性的类型。

需要定义为react提供的类型React.ComponentType。如果子组件里面本身有使用props,那么需要将这个props的类型加上,React.ComponentType<SonComponentProps>

先定义两个简单的组件:

要做的就是将Profile组件当作属性传到Private组件里面去,而不是像Login组件一样直接引入使用:

image-20251104184529127

但是我看到很多写法是这样的:

传递的是一个标签,这时候该怎么定义类型呢?

这种用法就是一个组件实例,使用React.ReactNode来定义类型。那么此时在Private里面接收到的参数,只能写成这样return Component,而不能写出这样return <Component />

为什么呢?下面会讲。

我们来深入地、形象地讲清楚——“组件类型(Component Type)”“组件实例(Component Instance)” 到底有什么区别。


🧩 一、先看个例子


🧠 二、类比理解

想象一下你在写面向对象的代码:

名称对应 React 中的概念说明
Dog组件类型(Component Type)定义了“狗是什么样”的模板
new Dog()组件实例(Component Instance)根据模板创建出来的一只狗

同理:

React 代码概念解释
Profile组件类型是一个函数或类,描述了要怎么渲染
<Profile />组件实例运行时由 React 创建的真实元素(JSX Element)

⚙️ 三、从运行角度看区别

✅ 组件类型(Component Type)

✅ 组件实例(Component Instance)


🧩 四、从 TypeScript 角度看

概念代码示例对应 TS 类型
组件类型ProfileReact.ComponentTypeReact.FC
组件实例<Profile />React.ReactElementReact.ReactNode

🧠 五、用途区别(非常关键)

使用场景例子用什么类型
想让别人传“组件类型”,我来控制它怎么渲染<Private component={Profile} />React.ComponentType
想让别人直接传好一个 JSX 元素<Private component={<Profile />} />React.ReactNode

🔍 六、形象比喻总结

类比React 概念举例
“食谱”组件类型Profile(描述菜怎么做)
“一道做好的菜”组件实例<Profile />(已经做好可以上桌)

你可以传“食谱”(类型),在容器组件里自己去决定什么时候、怎么做菜 也可以直接传“做好的一道菜”(实例),容器组件只负责摆上去。


✅ 七、小结

名称表示什么常用类型是否会被渲染
组件类型函数或类组件React.ComponentType❌ 不直接渲染
组件实例<Component />React.ReactElement / React.ReactNode✅ 可直接渲染

19 - Generic Props

在props里面怎么使用泛型。

定义一个列表组件,然后在App.tsx里面使用:

可以看到,使用没有问题。

 

但是当有一天,我需要将items里面的内容改为number类型,这时候就会报错:

image-20251104190705651

虽然可以修改List组件里面的ts类型定义,但是太麻烦了,以后或许会有更多的类型需要添加。

这时候就可以使用泛型generics:

extends的用法不要忘记了。

20 - Restricting Props

限制属性。这节课的意思是说,如果传递了某个属性,就限制别的属性不要传递,在组件使用时就做到这一点。

要求,在使用RandomNumber组件时,如果传递了isPositive属性,那么isNegative和isZero属性就不能传递,这两个属性使用时也一样。

这时候要用到ts里面的never类型。

可以看到,当写上别的属性时,会报错。

21 - Template Literals and Exclude

这节课学习模板字面量类型。之前我们已经学过了字符串字面量类型,目标字面量类型正是基于此。

字符串字面量类型type Direction = "north" | "south" | "east" | "west"。属于联合类型的一种。

那么在App.tsx中使用的时候,就能得到正确的提示:

但是里面出现了center-center,我们只需要center就够了,怎么办呢?这时候需要使用ts的一个特性Exclude

22 - Wrapping HTML Elements

这节课学习包裹原生HTML元素,创建自定义组件,同时学习接收HTML元素的属性props,定义类型。

比如说我们定义了一个Button组件,写成这样:

那么在App.tsx中使用的时候,可以这样写:

但是如果我们加上一些原生button的属性呢?比如说在button标签之间加上文字、加上onClick事件,会怎么样呢?

会报错,报错信息是ts没有定义类型。

当然可以在自定义Button里面加上类型,但是不推荐这样的写法。因为原生button上面有很多属性,可以一次性交叉进来,React.ComponentProps<"button">。这样写:

这样,在App.tsx中使用时,就不会有ts报错了。

 

同样,也可以用原生input元素创建一个react组件。

但是如果有一些属性类型,我们想限制一下,比如说children属性,只允许传递字符串过来,不允许传递组件过来,可以使用ts的Omit方法。

23 - Extracting a Components Prop Types

这节课学习怎么抽离一个组件的props的类型。

创建一个组件,在这个组件中,我们需要和Greet.tsx组件一样的props类型,怎么办?假设Greet里面的props类型没有导出。

使用React.ComponentProps<typeof Greet>

24 - Polymorphic Components

多态组件。在老师的react课程里面有。比如说mui里面的Typography就是一种多态组件,通过component属性可以渲染成不同的html元素。

一个多态组件是指:一个组件,可以动态地渲染成不同的底层 HTML 元素或另一个 React 组件,同时还能保持其自身的样式、逻辑和正确的类型安全。

比如说我创建了一个Text组件:

可以这样使用:

但是有一个问题,就是渲染出来的真实html都是div,我想渲染出来的真实html分别变为h1、p、label,可以做到吗?

可以做到,通过新增一个属性来代替div,注意类型是什么。

image-20251105091336937

可以看到,渲染的真实DOM变为了h1、p、label。

 

但是此时组件还不能完全处理html元素的属性,比如说我在as="label"组件上面加上htmlFor属性,它还是会有类型报错:

image-20251105091842428

解决办法:将html的属性加上去。记得html的属性是什么吗?上节课刚讲了,React.ComponentProps

image-20251105092227889

但是React.ComponentProps需要传递具体的html标签类型作为泛型参数,此时就可以加上。但是此时T的范围太宽泛了,所以此时需要使用extends来约束泛型,约束成什么样呢?那么因为as定义的类型是React.ElementType,所以只需要约束成这个类型即可。

image-20251105092707483

上面还有一个类型报错,先不管。TextOwnProps里面的所有属性和React.ComponentType里面的属性有重复,所以把React.ComponentType里面的相关属性去掉,使用Omit

image-20251105093119597

再来处理类型报错:

image-20251105093241333

可以看到,在as="label"上写htmlFor属性是正常的,但是在as="h1"上写这个属性会报错,因为h1上没有这个属性,这是相当智能了。

25 - Wrapping up

总结:

image-20251104210432317